Integrating Knockout.js with RESTful APIs allows you to fetch data from a server and update your UI dynamically. Here's a beginner's guide to fetching data and updating it using Knockout.js:
1. Setting Up Your HTML
Start by setting up your HTML structure where you want to display data fetched from the API.
<!doctype html>
<html lang="en">
<head>
<!-- Meta tags for character set and viewport configuration -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Title of the webpage -->
<title>Bootstrap demo</title>
<!-- Link to Bootstrap CSS for styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Link to Knockout.js library for MVVM pattern support -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"
integrity="sha512-vs7+jbztHoMto5Yd/yinM4/y2DOkPLt0fATcN+j+G4ANY2z4faIzZIOMkpBmWdcxt+596FemCh9M18NUJTZwvw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.4/knockout.validation.min.js"
integrity="sha512-b99MDNv5TqiZtPKH2UeHzDAVydmgrOJEPtaPPEF8AgV86eYyqINFI/K7/7f0+R4WNTAVv8KvvpjwfOYHv5rd5g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Link to jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<!-- Main container for the content -->
<div class="container my-3">
<!-- Header for the section -->
<h2>Data from Server</h2>
<div class="d-flex w-100 justify-content-between ">
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#postStaticBackdrop">
New Post
</button>
<div>
<input type="search" data-bind="value: searchText, valueUpdate: 'input'" class="form-control"
placeholder="Search ..." />
</div>
</div>
<hr />
<p class="fw-semibold text-black-50 text-end">
<span class="" data-bind="text: filteredItemsLength"></span>
<span>data found.</span>
</p>
<div data-bind="css: { 'd-none': filteredItemsLength() !== 0 }">
<hr/>
<div class="d-flex justify-content-center fw-bold">
<div class="text-center">
<p class=" fs-2 ">
<span class="me-2">😕</span>
No records were found
</p>
<!-- <p class="fw-normal">try somthing new keywords</p> -->
</div>
</div>
</div>
<!-- Responsive row to display items fetched from the server -->
<div class="row g-3 row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-4" data-bind="foreach: filteredItems">
<!-- Column for each item -->
<div class="col">
<!-- Card to display item details -->
<div class="card h-100">
<div class="card-body">
<!-- Card title bound to item's title -->
<h5 class="card-title text-capitalize" data-bind="text: title"></h5>
<!-- Card text bound to item's body -->
<p class="card-text" data-bind="text: body"></p>
</div>
<div class="card-footer border-0 pt-0">
<!-- Footer with item id and a link -->
<p class="m-0 d-flex justify-content-between">
<!-- Item id displayed -->
<span class="fs-5 fw-bold" data-bind="text: id"></span>
<!-- Link to comments page for the item -->
<a
data-bind="attr: { href: 'https://jsonplaceholder.typicode.com/posts/' + id + '/comments' }">
<!-- SVG icon inside the link -->
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor"
class="bi bi-arrow-right-circle-fill" viewBox="0 0 16 16">
<path
d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z" />
</svg>
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- Modal -->
<div class="modal fade" id="postStaticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="postStaticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="postStaticBackdropLabel">Add New Post</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="row g-3" data-bind="submit: function() { addPost({ title: newTitle(), body: newBody(), userId: userId() }); }">
<input type="hidden" data-bind="value: userId"/>
<div class="col-12">
<label class="form-label fw-bold">Title</label>
<input type="text" placeholder="Title" id="newTitle" name="newTitle" data-bind="value: newTitle, valueUpdate: 'input'" class="form-control">
</div>
<div class="col-12">
<label class="form-label fw-bold">Body</label>
<textarea rows="5" placeholder="Body" id="newBody" name="newBody" data-bind="value: newBody, valueUpdate: 'input'" class="form-control"></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Add Post</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Link to external JavaScript file -->
<script src="app.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</html>
2. Creating Your ViewModel
In your JavaScript file (app.js
), define your ViewModel, and include functions to fetch data from the API and update your UI.
// app.js
// Initialize Knockout validation
ko.validation.init({
registerExtenders: true, // Register custom validation rules
messagesOnModified: true, // Show validation messages as soon as a field is modified
insertMessages: true, // Insert validation messages next to the input elements
parseInputAttributes: true, // Parse HTML5 input attributes for validation rules
errorClass: 'text-danger fw-semibold', // CSS class for validation error messages
messageTemplate: null // Use default message template
}, true);
class AppViewModel {
constructor() {
var self = this;
// Observable array to hold the data fetched from the server
self.items = ko.observableArray([]);
self.searchText = ko.observable('');
self.newTitle = ko.observable('').extend({
required: { message: "Title is required." },
minLength: { params: 2, message: "Title must be at least 2 characters." }
});
self.newBody = ko.observable('').extend({
required: { message: "Body is required." },
minLength: { params: 2, message: "Body must be at least 2 characters." }
});
self.userId = ko.observable('11');
// Function to fetch data from the API
self.fetchData = function() {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(data => {
self.items(data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
};
// Computed observable to filter the items based on the filter criteria
self.filteredItems = ko.computed(function () {
//console.log(self.items().reverse());
// Reverse the items array
var reversedItems = self.items().slice().reverse();
//var filter = self.filter().toLowerCase(); // Convert filter criteria to lowercase
if (!self.searchText) {
return reversedItems; // If no filter, return all items
} else {
// Filter the items based on the filter criteria
return ko.utils.arrayFilter(reversedItems, function (item) {
return JSON.stringify(item).includes(self.searchText().toLowerCase()); // Case-insensitive match
});
}
});
// Function to reverse the observable array
self.reverseArray = function() {
self.items(self.items().slice().reverse());
};
self.filteredItemsLength = ko.computed(function () {
return self.filteredItems().length;
});
self.fetchData();
// Initialize validation
self.errors = ko.validation.group(self);
}
}
// Apply the Knockout bindings to the AppViewModel
ko.applyBindings(new AppViewModel());
Understanding the Code
HTML: <ul data-bind="foreach: posts">
: This binds to the
posts
observable array in your ViewModel, iterating over each post to display its
title
and body
.
JavaScript (ViewModel):
self.posts = ko.observableArray([]);
: This defines an observable arrayposts
to store fetched data.self.fetchData = function() { ... };
: This function usesfetch()
to make a GET request to the RESTful API (https://jsonplaceholder.typicode.com/posts
in this example). Upon successful response, it updates theposts
observable array with the fetched data.
Fetching Data:
fetch('https://jsonplaceholder.typicode.com/posts')
: Initiates a GET request to fetch data from the specified API endpoint..then(response => response.json())
: Converts the response to JSON format..then(data => { self.posts(data); })
: Updates theposts
observable array with the fetched data..catch(error => { console.error('Error fetching data:', error); });
: Handles any errors that occur during the fetch operation.
3. Updating Data and Sending Changes
To update data or send changes back to the server (POST, PUT, DELETE requests), you would typically define functions in your ViewModel similar to
fetchData()
but tailored for specific HTTP methods (fetch()
supports various HTTP methods).
// Function to add a new post
self.addPost = function(newPost) {
// Check if the form is valid
if (self.errors().length === 0) {
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
.then(response => response.json())
.then(data => {
self.items.push(data); // Assuming the server responds with the created post object
self.newTitle(''); // Clear input after successful addition
self.newBody(''); // Clear input after successful addition
// Hide modal using jQuery
$('#postStaticBackdrop').modal('hide');
self.errors.showAllMessages(false); // Hide all validation messages
})
.catch(error => {
console.error('Error adding post:', error);
});
} else {
// Show validation errors
self.errors.showAllMessages(true);
}
};
Output -
Leave Comment